package com.yzi.core.json
{
	public class JSONTokenizer
	{
		private var m_object:Object;
		private var m_jsonString:String;
		private var m_location:int;
		private var m_char:String;
		
		public function JSONTokenizer($jsonString:String)
		{
			m_jsonString = $jsonString;
			m_location = 0;
			
			nextChar();
		}
		
		private function readString(): JSONToken
		{
			var result:JSONToken = new JSONToken();
			result.type = JSONTokenType.STRING;
			
			var string:String = "";
			// advance past the first "
			nextChar();
			
			while(m_char != '"' && m_char != '')
			{
				if (m_char == '\\' ) {					
					nextChar();
					
					switch (m_char) {
						case '"': // quotation mark
							string += '"';
							break;
						case '/':	// solidus
							string += "/";
							break;
						case '\\':	// reverse solidus
							string += '\\';
							break;
						case 'b':	// bell
							string += '\b';
							break;
						case 'f':	// form feed
							string += '\f';
							break;
						case 'n':	// newline
							string += '\n';
							break;
						case 'r':	// carriage return
							string += '\r';
							break;
						case 't':	// horizontal tab
							string += '\t'
							break;
						case 'u':
							var hexValue:String = "";
							
							for(var i:int = 0; i < 4; ++i)
							{
								if(isHexDigit(nextChar()) == false) {
									ParseError(" Excepted a hex digit, but found: " + m_char);
								}
								hexValue += m_char;
							}
							string += String.fromCharCode(parseInt(hexValue, 16));
							
							break;
						default:
							string += '\\' + m_char;
							break;
					}
				} else {
					string += m_char;
				}
				
				nextChar();
			}

			if(m_char == '') {
				ParseError("Unterminated string literal");
			}
			
			nextChar();
			result.value = string;
			
			return result;
		}
		
		private function readNumber(): JSONToken
		{
			var input:String = "";
			var result:JSONToken = new JSONToken();
			result.type = JSONTokenType.NUMBER;
			
			if(m_char == '-') {
				input += '-';
				nextChar();
			}
			
			if(isDigit(m_char) == false) {
				ParseError("Expecting a digit");
			}
			
			if(m_char == '0') {
				input += m_char;
				nextChar();
				
				if(isDigit(m_char)) {
					ParseError("A digit cannot immediately follow 0");
				}
			} else {
				while(isDigit(m_char))
				{
					input += m_char;
					nextChar();
				}
			}
			
			if(m_char == '.') {
				input += '.';
				nextChar();

				if(isDigit(m_char) == false) {
					ParseError("Expecting a digit");
				}
				
				while(isDigit(m_char))
				{
					input += m_char;
					nextChar();
				}
			}
			
			if(m_char == 'e' || m_char == 'E' ) {
				input += "e"
				nextChar();
				if (m_char == '+' || m_char == '-') {
					input += m_char;
					nextChar();
				}
				
				if (isDigit(m_char) == false) {
					ParseError("Scientific notation number needs exponent value");
				}
				
				while(isDigit(m_char))
				{
					input += m_char;
					nextChar();
				}
			}
			
			// convert the string to a number value
			var num:Number = Number(input);
			
			if(isFinite(num) && !isNaN(num)) {
				result.value = num;
				return result;
			} else {
				ParseError("Number " + num + " is not valid!");
			}
			return result;
		}
		
		private function nextChar(): String { return m_char = m_jsonString.charAt(m_location++); }
		
		private function skipIgnore(): void
		{
			var originalLocation:int;
			
			do {
				originalLocation = m_location;
				skipWhite();
				skipComments();
			} while(originalLocation != m_location);
		}
		
		private function skipComments(): void
		{
			if(m_char == '/') {
				nextChar();
				switch(m_char) {
					case '/':
						do {
							nextChar();
						} while(m_char != '\n' && m_char != '')
						
						nextChar();
						break;
					case '*':
						nextChar();
						
						while(true) {
							if(m_char == '*') {
								nextChar();
								if (m_char == '/') {
									nextChar();
									break;
								}
							} else {
								nextChar();
							}
							
							if(m_char == '') {
								ParseError("Multi-line comment not closed");
							}
						}
						break;
					
					default:
						ParseError("Unexpected " + m_char + " encountered (expecting '/' or '*' )" );
				}
			}
		}
		
		private function skipWhite(): void
		{
			while(isWhiteSpace(m_char))
			{
				nextChar();
			}
		}
		
		private function isWhiteSpace($char:String): Boolean
		{
			return (m_char == ' ' || m_char == '\t' || m_char == '\n' || m_char == '\r');
		}
		
		private function isDigit($char:String): Boolean { return (m_char >= '0' && m_char <= '9'); }
		
		private function isHexDigit($char:String): Boolean
		{
			var upperCase:String = m_char.toUpperCase();
			
			return (isDigit(m_char) || (upperCase >= 'A' && upperCase <='F'));
		}
		
		public function GetNextToken(): JSONToken
		{
			var result:JSONToken = new JSONToken();
			skipIgnore();
			
			switch(m_char) {
				case '{':
					result.type = JSONTokenType.LEFT_BRACE;
					result.value = '{';
					nextChar();
					break;
				case '}':
					result.type = JSONTokenType.RIGHT_BRACE;
					result.value = '}';
					nextChar();
					break;
				case '[':
					result.type = JSONTokenType.LEFT_BRACKET;
					result.value = '[';
					nextChar();
					break;
				case ']':
					result.type = JSONTokenType.RIGHT_BRACKET;
					result.value = ']';
					nextChar();
					break
				case ',':
					result.type = JSONTokenType.COMMA;
					result.value = ',';
					nextChar();
					break;
				case ':':
					result.type = JSONTokenType.COLON;
					result.value = ':';
					nextChar();
					break;
				case 't': // attempt to read true
					var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();
					
					if (possibleTrue == "true") {
						result.type = JSONTokenType.TRUE;
						result.value = true;
						nextChar();
					} else {
						ParseError("Expecting 'true' but found " + possibleTrue);
					}
					
					break;
				case 'f': // attempt to read false
					var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();
					
					if (possibleFalse == "false") {
						result.type = JSONTokenType.FALSE;
						result.value = false;
						nextChar();
					} else {
						ParseError("Expecting 'false' but found " + possibleFalse);
					}
					
					break;
				case 'n': // attempt to read null
					var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();
					
					if (possibleNull == "null") {
						result.type = JSONTokenType.NULL;
						result.value = null;
						nextChar();
					} else {
						ParseError("Expecting 'null' but found " + possibleNull);
					}
					
					break;
				case '"': // the start of a string
					result = readString();
					break;
				default: 
					if (isDigit(m_char) || m_char == '-') {
						result = readNumber();
					} else if (m_char == '') {
						return null;
					} else {
						ParseError("Unexpected " + m_char + " encountered");
					}
			}
			return result;
		}
		
		public function ParseError($message:String): void { throw new JSONParseError($message, m_location, m_jsonString); }
	}
}